package comnet.ftpclient;

import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.net.ConnectException;
import java.net.Socket;
import java.util.Arrays;
import java.util.Vector;

/**
 * @author Mikko Puustinen (mikko.puustinen@gmail.com)
 * @version 0.1, 2007-nov-25
 */
public class FtpClient {
    private String host;
    private String serverName;
    private int port;
    private String username;
    private String password;
    private Socket socket;
    private BufferedReader reader;
    private PrintWriter writer;
    private ChangeListener listenerClass;
    private Vector<String> fileList;
    private String currentDir = "";
    private String lastReply;

    /**
     *
     */
    public FtpClient() {
        fileList = new Vector<String>();
    }

    /**
     * @param host
     * @param port
     */
    public FtpClient(String host, int port) {
        this();
        this.host = host;
        this.port = port;
    }

    /**
     * @param host
     * @param port
     * @param username
     * @param password
     */
    public FtpClient(String host, int port, String username, String password) {
        this(host, port);
        this.username = username;
        this.password = password;
    }

    /**
     * @param username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * @param port
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * @param host
     */
    public void setHost(String host) {
        this.host = host;
    }

    /**
     * @return
     */
    private String getPassword() {
        return password;
    }

    /**
     * @return
     */
    public String getUsername() {
        return username;
    }

    /**
     * @return the name of the server.
     */
    public String getServerName() {
        return serverName;
    }

    /**
     * Creates a connection to the host.
     *
     * @return true if the connection is established, false otherwise.
     *
     * @throws java.io.IOException if the socket creation failures.
     */
    public boolean setConnection() throws IOException {
        try {
            socket = new Socket(host, port);
        } catch (ConnectException e) {
            throw new ConnectException("Host unreachable");
        }
        reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        writer = new PrintWriter(socket.getOutputStream(), true);

        getReply();

        listenerClass.changedDirectory();
        return isLastReplyPositive();
    }

    /**
     * @throws IOException
     */
    private void getReply() throws IOException {
        String line = readLine();
        //TODO: Use more appropriate exceptions
        if (line == null) {
            throw new IOException("Connection closed.");
        }

        if (line.length() < 2) {
            throw new IOException("Error in server reply: " + line);
        }

        lastReply = line;

        listenerClass.newServerReply();
    }

    /**
     * @return
     */
    public boolean isLastReplyPositive() {
        boolean b = false;
        if (lastReply.startsWith("OK")) {
            b = true;
        }

        return b;
    }

    /**
     * @return
     */
    public String getLastReply() {
        return lastReply;
    }

    /**
     * Get the last message from server, for example if the last reply was
     * <code>OK All good</code> this method will return <code>All good</code>.
     * If there is no message an empty String will be returned.
     *
     * @return
     */
    public String getLastServerMessage() {
        String line = getLastReply();
        if (line.matches("(?i)(OK|ERROR)\\s.+")) {
            return line.substring(line.indexOf(0x20) + 1);
        }

        return "";
    }

    /**
     * Get the data port from the last reply.
     *
     * @return the data port.
     */
    private int getDataPort() {
        return Integer.parseInt(getLastReply().substring(3));
    }

    /**
     * @param msg
     *
     * @return
     */
    public String sendLine(String msg) {
        writer.println(msg);
        writer.flush();
        try {
            getReply();
        } catch (IOException e) {
            //
        }

        return getLastReply();
    }

    /**
     * @return
     *
     * @throws java.io.IOException
     */
    public String readLine() throws IOException {
        return reader.readLine();
    }

    /**
     * @param file
     *
     * @throws java.io.IOException
     */
    //FIXME: Refactor (method: getNumberOfBytes)
    public void uploadFile(File file) throws IOException {
        Socket dataSocket;
        int numberOfBytes = (int)file.length();
        BufferedInputStream is = null;
        DataOutputStream os;

        sendLine("PUT " + file.getName());

        if (!isLastReplyPositive()) {
            return;
        }
        dataSocket = createConnection();
        os = new DataOutputStream(dataSocket.getOutputStream());
        os.writeInt(numberOfBytes);
        os.writeInt(0x20);

        try {
            is = new BufferedInputStream(new FileInputStream(file));
            fileHandling(is, os, numberOfBytes, dataSocket.getSendBufferSize());
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    /**
     * @param remoteFile
     * @param file
     *
     * @throws java.io.IOException
     */
    //FIXME: Refactor
    public void downloadFile(String remoteFile, File file) throws IOException {
        Socket dataSocket;
        int numberOfBytes;
        DataInputStream input;
        BufferedOutputStream os = null;

        sendLine("GET " + remoteFile);

        if (!isLastReplyPositive()) {
            return;
        }
        dataSocket = createConnection();
        input = new DataInputStream(dataSocket.getInputStream());
        numberOfBytes = input.readInt();
        input.readInt();

        try {
            os = new BufferedOutputStream(new FileOutputStream(file));
            fileHandling(input, os, numberOfBytes, dataSocket.getReceiveBufferSize());
        } finally {
            if (os != null) {
                os.close();
            }
        }
    }

    /**
     * @param is
     * @param os
     * @param numberOfBytes
     * @param bufferSize
     *
     * @throws IOException
     */
    @SuppressWarnings({"MethodMayBeStatic"})
    private void fileHandling(InputStream is, OutputStream os, final int numberOfBytes, final int bufferSize) throws IOException {
        byte[] buffer = new byte[bufferSize];
        int bytesRead = 0;
        int length;

        while ((length = is.read(buffer)) > 0 && bytesRead < numberOfBytes) {
            os.write(buffer, 0, length);
            bytesRead += length;
        }

        os.flush();
        os.close();
        is.close();
    }

    public void changeDirectory(String dir) {
        sendLine("CD " + dir);
        if (isLastReplyPositive()) {
            listenerClass.changedDirectory();
        }
    }

    public void upOneDirectory() {
        changeDirectory("..");
    }

    /**
     * @return
     *
     * @throws IOException
     */
    @Nullable
    //FIXME: Refactor (method: getNumberOfBytes)
    public Object[] getList() throws IOException {
        Socket dataSocket;
        ByteArrayOutputStream baos;
        DataInputStream input;
        int numberOfBytes;

        sendLine("LIST");

        if (!isLastReplyPositive()) {
            return null;
        }
        dataSocket = createConnection();

        baos = new ByteArrayOutputStream();
        input = new DataInputStream(dataSocket.getInputStream());
        numberOfBytes = input.readInt();
        input.readInt();

        fileHandling(dataSocket.getInputStream(), baos, numberOfBytes, dataSocket.getReceiveBufferSize());
        fileList.clear();
        String[] lines = baos.toString().split(":");
        fileList.addAll(Arrays.asList(lines));

        return fileList.toArray();
    }

    public Socket createConnection() throws IOException {
        int dataPort = 0;
        try {
            dataPort = getDataPort();
        } catch (NumberFormatException e) {
            System.err.println("createConnection: numberFormatException:\t>>" + getLastReply());
        }

        return new Socket(host, dataPort);
    }

    public void addChangeListenerClass(ChangeListener listener) {
        this.listenerClass = listener;
    }

    /**
     * @throws IOException
     */
    public void close() throws IOException {
        if (socket != null && reader != null && writer != null) {
            sendLine("QUIT");
            socket.close();
            reader.close();
            writer.close();
        }
    }
}